Wednesday 8 September 2021

Web Tutorial: The COVID-19 Dashboard (Part 1/4)

It's D3 time! Been a while since I did D3, but let's have some fun amid the ongoing COVID-19 pandemic. You see, for the past year and a half now, I've been collecting data about Singapore's COVID-19 cases. And it's now time to put all that lovely, lovely data to good use.

Here's a sample of the data in CSV format. The entire dataset can be found here.
Day,Imports,MW,SG,Deaths
2020/04/01,20,0,54,0
2020/04/02,8,0,41,1
2020/04/03,0,0,65,1
2020/04/04,6,0,69,1
.
.
.


This is what the data represents:
Day - The date in which the data takes place on. There is one entry for every day of each month.
SG - The number of discovered COVID-19 cases that were Singapore citizens on that day.
MW - The number of discovered COVID-19 cases that were migrant workers on that day.
Imports - The number of discovered COVID-19 cases that were foreigners that traveled to Singapore on that day.
Deaths - The number of people who passed away due to COVID-19 on that day.

Caution

Although this is JavaScript, a server is required because we will be using an asynchronous operation to pull data. We could possibly get around this by turning off security settings, but why take that chance, really?

We begin with some HTML. We will use Verdana for the font and set the outline property of all divs to red, for some visibility. We have also included the link to the D3 library, with a script tag in the body.
<!DOCTYPE html>
<html>
    <head>
        <title>COVID-19 Dashboard</title>

        <style>
            body
            {
                font-family: verdana;
            }

            div
            {
                outline: 1px solid red;
            }
        </style>

        <script src="https://d3js.org/d3.v4.min.js"></script>
    </head>

    <body>
        <script>

        </script>
    </body>
</html>


Now, we have a div styled using the CSS class dashboardContainer. Within it, we have three divs styled using the CSS classes topContainer, middleContainer and bottomContainer respectively.
<body>
    <div class="dashboardContainer">
        <div class="topContainer">

        </div>

        <div class="middleContainer">

        </div>

        <div class="bottomContainer">

        </div>

    </div>

    <script>

    </script>
</body>


Here are some styles. We set the height of dashboardContainer and center it in the screen. topContainer, middleContainer and bottomContainer will each take up full width and float left, with varying heights. middleContainer and bottomContainer will have the margin-top property set.
<style>
    body
    {
        font-family: verdana;
    }

    div
    {
        outline: 1px solid red;
    }

    .dashboardContainer
    {
        width: 800px;
        height: 620px;
        margin: 0 auto 0 auto;
    }

    .topContainer
    {
        width: 100%;
        height: 250px;
        float: left;
    }

    .middleContainer
    {
        width: 100%;
        height: 50px;
        margin-top: 10px;
        float: left;
    }

    .bottomContainer
    {
        width: 100%;
        height: 300px;
        margin-top: 10px;
        float: left;
    }

</style>


And immediately, you get a promising-looking layout!


Then in the divs styled by middleContainer and bottomContainer, insert two divs each styled by leftContainer and rightContainer, respectively.
<div class="dashboardContainer">
    <div class="topContainer">

    </div>

    <div class="middleContainer">
        <div class="leftContainer">

        </div>

        <div class="rightContainer">

        </div>

    </div>

    <div class="bottomContainer">
        <div class="leftContainer">

        </div>

        <div class="rightContainer">

        </div>

    </div>
</div>


Now we style these. They float left and right respectively, and have roughly half the width of their parents, and all of the height.
.bottomContainer
{
    width: 100%;
    height: 300px;
    margin-top: 10px;
    float: left;
}

.leftContainer
{
    width: 395px;
    height: 100%;
    float: left;
}

.rightContainer
{
    width: 395px;
    height: 100%;
    float: right;
}




So now, in this div, add a p tag and the following elements within - a string, two buttons and a drop-down list. Add ids for the buttons and the drop-down list.
<div class="middleContainer">
    <div class="leftContainer">
        <p>
            COVID-19 DASHBOARD
            <input type="button" id="btnPrev" value="<<" />
            <select id="ddlPeriod"></select>
            <input type="button" id="btnNext" value=">>" />
        </p>

    </div>

    <div class="rightContainer">

    </div>
</div>


This is a preview of what your main controls on your dashboard will look like.


Add a series of checked checkboxes with strings as labels, in the other div.
<div class="middleContainer">
    <div class="leftContainer">
        <p>
            COVID-19 DASHBOARD
            <input type="button" id="btnPrev" value="<<" />
            <select id="ddlPeriod"></select>
            <input type="button" id="btnNext" value=">>" />
        </p>
    </div>

    <div class="rightContainer">
        <input type="checkbox" checked /> Singapore Citizens
        <input type="checkbox" checked /> Migrant Workers
        <input type="checkbox" checked /> Imports
        <input type="checkbox" checked /> Deaths

    </div>
</div>


It's a little messy and we'll be cleaning this up.


Each of these checkboxes and their labels should go into a div with the class statContainer.
<div class="rightContainer">
    <div class="statContainer">
        <input type="checkbox" checked /> Singapore Citizens
    </div>
    <div class="statContainer">
        <input type="checkbox" checked /> Migrant Workers
    </div>
    <div class="statContainer">
        <input type="checkbox" checked /> Imports
    </div>
    <div class="statContainer">
        <input type="checkbox" checked /> Deaths
    </div>
</div>


statContainer will have width and height set, float left and the font will be set as well, but that last part is purely aesthetic.
.rightContainer
{
    width: 395px;
    height: 100%;
    float: right;
}

.statContainer
{
    width: 45%;
    height: 40%;
    float: left;
    font-size: 0.8em;
    font-weight: bold;    
}


And now we have a better layout.


How about we color-code these? Add a class to each of these divs.
<div class="rightContainer">
    <div class="statContainer colSG">
        <input type="checkbox" checked /> Singapore Citizens
    </div>
    <div class="statContainer colMW">
        <input type="checkbox" checked /> Migrant Workers
    </div>
    <div class="statContainer colImports">
        <input type="checkbox" checked /> Imports
    </div>
    <div class="statContainer colDeaths">
        <input type="checkbox" checked /> Deaths
    </div>
</div>


Each of these CSS classes has a different color.
.statContainer
{
    width: 45%;
    height: 40%;
    float: left;
    font-size: 0.8em;
    font-weight: bold;    
}

.colSG { color: #FF0000; }
.colMW { color: #9999FF; }
.colImports { color: #44AA44; }
.colDeaths { color: #999999; }


And here we see that SG data is red, MW data is pale blue, Imports data is green and Deaths data is grey. This should be consistent throughout the dashboard.


Now for data!

We'll go right into the script tag. First, we create the object dashboard. In it, we have the following properties.

keys - an array that will hold all the possible month-year combinations of the dataset, so as to be able to traverse the array covidData easily.
currentKey - a value that will be one of the values within keys. It defaults to undefined.
covidData - an array that holds your entire dataset.
totalSG - the pre-calculated sum of all Singapore Citizen cases. The initial value is 0.
totalMW - the pre-calculated sum of all migrant worker cases. The initial value is 0.
totalImports - the pre-calculated sum of all imported cases. The initial value is 0.
totalDeaths - the pre-calculated sum of all deaths. The initial value is 0.

<script>
    let dashboard =
    {
        keys: [],
        currentKey: undefined,
        covidData: [],
        totalSG: 0,
        totalMW: 0,
        totalImports: 0,
        totalDeaths: 0
    };

</script>


Here, we use the csv() method of the d3 object. Within it, we pass the name of our CSV file, which is covid19.csv, and a callback. data is a parameter which represents the CSV data extracted from the file. data consists of an array of objects. Each object represents a line in the CSV data.
<script>
    let dashboard =
    {
        keys: [],
        currentKey: undefined,
        covidData: [],
        totalSG: 0,
        totalMW: 0,
        totalImports: 0,
        totalDeaths: 0
    };

    d3.csv("covid19.csv", function(data)
    {

    });

</script>


Here, we are going to manipulate the data into a form that our dashboard can easily parse. We first use a For loop to iterate through the contents of data. We then define the variable, key, and use the getKey() method of the dashboard object, passing in the Day property of the current object.
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);
    }

});


Now we define the getKey() method. It will accept a string, x, as a parameter. This is the date string which is the Day property.
let dashboard =
{
    keys: [],
    currentKey: undefined,
    covidData: [],
    totalSG: 0,
    totalMW: 0,
    totalImports: 0,
    totalDeaths: 0,
    getKey: function(x)
    {

    }

};


Each Day string is in the format YYYY/MM/DD. So we first define a variable, elems, as an array derived from running x through the split() method and using "/" as an argument. The variable year is the first element of elems. The variable month is the second element of elems, minus 1 because we are going to use this value as a pointer to an array. The array is monthNames, and it contains all the month names of the calendar.
let dashboard =
{
    keys: [],
    currentKey: undefined,
    covidData: [],
    totalSG: 0,
    totalMW: 0,
    totalImports: 0,
    totalDeaths: 0,
    getKey: function(x)
    {
        var elems = x.split("/");
        var year = elems[0];
        var month = parseInt(elems[1]) - 1;
        var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    }
};


We return the element of monthNames pointed to by month, and year, in a string. So if we passed in "2020/04/03" as an argument, we would get "Apr 2020".
let dashboard =
{
    keys: [],
    currentKey: undefined,
    covidData: [],
    totalSG: 0,
    totalMW: 0,
    totalImports: 0,
    totalDeaths: 0,
    getKey: function(x)
    {
        var elems = x.split("/");
        var year = elems[0];
        var month = parseInt(elems[1]) - 1;
        var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

        return monthNames[month] + " " + year;
    }
};


Now that we've defined getKey(), let's move on. We use an If block to check if the current value of currentKey is undefined.
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);

        if (dashboard.currentKey == undefined)
        {

        }
        else
        {

        }
    
    }
});


If so, we set currentKey to the value of key, and create a new sub-array under covidData using key as the key. And then we push key into keys.
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);

        if (dashboard.currentKey == undefined)
        {
            dashboard.currentKey = key;
            dashboard.covidData[key] = [];
            dashboard.keys.push(key);

        }
        else
        {

        }    
    }
});


Otherwise, we first check if currentKey is equal to key. If not, we repeat what we did earlier.
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);

        if (dashboard.currentKey == undefined)
        {
            dashboard.currentKey = key;
            dashboard.covidData[key] = [];
            dashboard.keys.push(key);
        }
        else
        {
            if (dashboard.currentKey != key)
            {
                dashboard.currentKey = key;
                dashboard.covidData[key] = [];
                dashboard.keys.push(key);
            }

        }    
    }
});


Now, we start pushing the entire contents of the current element of data into that sub-array. What this does is that this will segregate data into sub-arrays delineated by month-year combinations.
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);

        if (dashboard.currentKey == undefined)
        {
            dashboard.currentKey = key;
            dashboard.covidData[key] = [];
            dashboard.keys.push(key);
        }
        else
        {
            if (dashboard.currentKey != key)
            {
                dashboard.currentKey = key;
                dashboard.covidData[key] = [];
                dashboard.keys.push(key);
            }
        }    

        dashboard.covidData[key].push({"sg": data[i].SG, "imports": data[i].Imports, "mw": data[i].MW, "deaths": data[i].Deaths});
    }
});


At this moment, we will take advantage of the For loop to increment the values of totalSG, totalMW, totalImports and totalDeaths accordingly. At the end of it, we should get the totals of the entire dataset. These will be useful later!
d3.csv("covid19.csv", function(data)
{
    for (var i = 0; i < data.length; i++)
    {
        var key = dashboard.getKey(data[i].Day);

        if (dashboard.currentKey == undefined)
        {
            dashboard.currentKey = key;
            dashboard.covidData[key] = [];
            dashboard.keys.push(key);
        }
        else
        {
            if (dashboard.currentKey != key)
            {
                dashboard.currentKey = key;
                dashboard.covidData[key] = [];
                dashboard.keys.push(key);
            }
        }    

        dashboard.covidData[key].push({"sg": data[i].SG, "imports": data[i].Imports, "mw": data[i].MW, "deaths": data[i].Deaths});

        dashboard.totalSG = dashboard.totalSG + parseInt(data[i].SG);
        dashboard.totalMW = dashboard.totalMW + parseInt(data[i].MW);
        dashboard.totalImports = dashboard.totalImports + parseInt(data[i].Imports);
        dashboard.totalDeaths = dashboard.totalDeaths + parseInt(data[i].Deaths);

    }
});


Outside of the For loop, define variable ddlPeriod and use the select() method of the d3 object to get the drop-down list ddlPeriod.
    dashboard.totalSG = dashboard.totalSG + parseInt(data[i].SG);
    dashboard.totalMW = dashboard.totalMW + parseInt(data[i].MW);
    dashboard.totalImports = dashboard.totalImports + parseInt(data[i].Imports);
    dashboard.totalDeaths = dashboard.totalDeaths + parseInt(data[i].Deaths);
}

var ddlPeriod = d3.select("#ddlPeriod");


Now we use the keys object (which we should have populated with all the possible month-year combinations in the dataset) to fill the drop-down list with option tags.
var ddlPeriod = d3.select("#ddlPeriod");

ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option");


We'll use the actual value as the text, but use the element number as the value. And by default, we will select the first value.
ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d, i)
{
    return i;
})
.text(function(d)
{
    return d;
})
;


And see now, we've populated the drop-down list!


We follow up by calling the drawCharts() method of the dashboard object.
ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d, i)
{
    return i;
})
.text(function(d)
{
    return d;
});

dashboard.drawCharts();


We are going to define the drawCharts() method soon. But first, make sure that it fires off whenever the checkboxes are clicked, using the onclick attribute. We will also add an extra class to each containing div so that it is easily identifiable by the drawCharts() method.
<div class="rightContainer">
    <div class="statContainer cb_sg colSG">
        <input type="checkbox" onclick="dashboard.drawCharts();" checked/> Singapore Citizens
    </div>
    <div class="statContainer cb_mw colMW">
        <input type="checkbox" onclick="dashboard.drawCharts();" checked/> Migrant Workers
    </div>
    <div class="statContainer cb_imports colImports">
        <input type="checkbox" onclick="dashboard.drawCharts();" checked/> Imports
    </div>
    <div class="statContainer cb_deaths colDeaths">
        <input type="checkbox" onclick="dashboard.drawCharts();" checked/> Deaths
    </div>    
</div>


Let us define the drawCharts() method now.
getKey: function(x)
{
    var elems = x.split("/");
    var year = elems[0];
    var month = parseInt(elems[1]) - 1;
    var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    return monthNames[month] + " " + year;
},
drawCharts: function()
{

}


Here, we select ddlPeriod. Then we define data as a subset of covidData, using the selected value of ddlPeriod. It is an integer, but when we use that integer as a pointer to reference covidData, we get the key! And it's this key that we use to get the data of, say, April 2020.
drawCharts: function()
{
    var ddlPeriod = d3.select("#ddlPeriod");

    var data = this.covidData[this.keys[ddlPeriod.node().value]];

}


Then we'll define sg, mw, imports and deaths as Boolean values based on which checkboxes are checked. So if the user only wishes to view Deaths and Imports, he would only check thse checkboxes. And the dashbard will reflect thse choices.
drawCharts: function()
{
    var ddlPeriod = d3.select("#ddlPeriod");
    var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
    var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
    var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
    var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");


    var data = this.covidData[this.keys[ddlPeriod.node().value]];
}


Then we will call the drawLineChart(), drawDonutChart() and drawBarChart() methods, passing in the variables we defined, as arguments. Note that drawDonutChart() does not use deaths.
drawCharts: function()
{
    var ddlPeriod = d3.select("#ddlPeriod");
    var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
    var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
    var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
    var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");

    var data = this.covidData[this.keys[ddlPeriod.node().value]];

    dashboard.drawLineChart(data, sg, mw, imports, deaths);
    dashboard.drawDonutChart(data, sg, mw, imports);
    dashboard.drawBarChart(data, sg, mw, imports, deaths);

}


And we will just populate the dashboard object with these methods.
drawCharts: function()
{
    var ddlPeriod = d3.select("#ddlPeriod");
    var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
    var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
    var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
    var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");

    var data = this.covidData[this.keys[ddlPeriod.node().value]];

    dashboard.drawLineChart(data, sg, mw, imports, deaths);
    dashboard.drawDonutChart(data, sg, mw, imports);
    dashboard.drawBarChart(data, sg, mw, imports, deaths);
},
drawLineChart: function(data, sg, mw, imports, deaths)
{

},
drawDonutChart: function(data, sg, mw, imports)
{

},
drawBarChart: function(data, sg, mw, imports, deaths)
{

}


After calling the drawCharts() method, make sure it runs whenever ddlPeriod changes in value.
dashboard.drawCharts();

d3.select("#ddlPeriod").on("change", function() { dashboard.drawCharts(); });


When the buttons are clicked, we need to run prev() and next() methods before running drawCharts().
dashboard.drawCharts();

d3.select("#ddlPeriod").on("change", function() { dashboard.drawCharts(); });
d3.select("#btnPrev").on("click", function() { dashboard.prev(); dashboard.drawCharts(); });
d3.select("#btnNext").on("click", function() { dashboard.next(); dashboard.drawCharts(); });


Now let us create the prev() and next() methods.
getKey: function(x)
{
    var elems = x.split("/");
    var year = elems[0];
    var month = parseInt(elems[1]) - 1;
    var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    return monthNames[month] + " " + year;
},
prev: function()
{

},
next: function()
{

}
,
drawCharts: function()
{


Now it's just a matter of getting the current value of ddlPeriod, and set it to currentVal.
prev: function()
{
    var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
},
next: function()
{

},


Since this basically sets the pointer to the previous value and the first value is 0, we only proceed if currentVal is greater than 0.
prev: function()
{
    var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
    if (currentVal > 0)
    {

    }

},
next: function()
{

},


We decrement currentVal, and set the value of ddlPeriod to currentVal.
prev: function()
{
    var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
    if (currentVal > 0)
    {
        currentVal = currentVal - 1;
        d3.select("#ddlPeriod").property("value", currentVal);

    }
},
next: function()
{

},


Now we do pretty much the same for next(), except that we increment currentVal, and only if currentVal is lesser than the maximum value.
prev: function()
{
    var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
    if (currentVal > 0)
    {
        currentVal = currentVal - 1;
        d3.select("#ddlPeriod").property("value", currentVal);
    }
},
next: function()
{
    var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
    if (currentVal < this.keys.length - 1)
    {
        currentVal = currentVal + 1;
        d3.select("#ddlPeriod").property("value", currentVal);
    }

},


Now if you click those buttons, you can see the selected value in the drop-down list change! And once you reach "Apr 2020" or "Aug 2021" (the first and last values respectively), the value no longer changes.

That's all for now. But we have created a good framework for what we're about to do next.

Next

Drawing the line chart.

No comments:

Post a Comment